<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"> 
<html>
<head>
<title>Java games Sokoban</title>
<link rel="stylesheet" href="/cfg/format.css" type="text/css">
<meta http-equiv="content-type" content="text/html; charset=utf-8">
<meta name="keywords" content="Java, Swing, games, tutorial, Sokoban, programming, GUI, multiplatform">
<meta name="description" content="This part of the Java games tutorial presents the Sokoban game.">
<meta name="language" content="en">
<meta name="author" content="Jan Bodnar">
<meta name="distribution" content="global">

<script type="text/javascript" src="/lib/jquery.js"></script>
<script type="text/javascript" src="/lib/common.js"></script>

</head>

<body>

<div class="container">

<div id="wide_ad" class="ltow">
<script type="text/javascript"><!--
google_ad_client = "pub-9706709751191532";
/* 160x600, August 2011 */
google_ad_slot = "2484182563";
google_ad_width = 160;
google_ad_height = 600;
//-->
</script>
<script type="text/javascript"
src="http://pagead2.googlesyndication.com/pagead/show_ads.js">
</script>
</div>

<div class="content">

<a href="/" title="Home">Home</a>&nbsp;
<a href="..">Contents</a>


<h1>Sokoban</h1>


<p>
In this part of the Java 2D games tutorial, we will create a Java Sokoban game clone. 
</p>

<script type="text/javascript"><!--
google_ad_client = "ca-pub-9706709751191532";
/* LargeSquare */
google_ad_slot = "5070696322";
google_ad_width = 336;
google_ad_height = 280;
//-->
</script>
<script type="text/javascript"
src="http://pagead2.googlesyndication.com/pagead/show_ads.js">
</script>

<h2>Sokoban</h2>

<p>
<b>Sokoban</b> is another classic computer game. 
It was created in 1980 by Hiroyuki Imabayashi. Sokoban means
a warehouse keeper in Japanese. The player pushes boxes around a 
maze. The objective is to place all boxes in designated locations.
</p>


<h2>Development</h2>

<p>
We control the sokoban object with cursor keys. We can also 
press the R key to restart the level. When all bags are placed 
on the destination areas, the game is finished. We draw "Completed"
string in the left upper corner of the window. 
</p>

<div class="codehead">Board.java</div>
<pre class="code">
package sokoban;

import java.awt.Color;
import java.awt.Graphics;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.util.ArrayList;
import javax.swing.JPanel;

public class Board extends JPanel { 

    private final int OFFSET = 30;
    private final int SPACE = 20;
    private final int LEFT_COLLISION = 1;
    private final int RIGHT_COLLISION = 2;
    private final int TOP_COLLISION = 3;
    private final int BOTTOM_COLLISION = 4;

    private ArrayList walls = new ArrayList();
    private ArrayList baggs = new ArrayList();
    private ArrayList areas = new ArrayList();
    private Player soko;
    private int w = 0;
    private int h = 0;
    private boolean completed = false;
    
    private String level =
              "    ######\n"
            + "    ##   #\n"
            + "    ##$  #\n"
            + "  ####  $##\n"
            + "  ##  $ $ #\n"
            + "#### # ## #   ######\n"
            + "##   # ## #####  ..#\n"
            + "## $  $          ..#\n"
            + "###### ### #@##  ..#\n"
            + "    ##     #########\n"
            + "    ########\n";

    public Board() {

        addKeyListener(new TAdapter());
        setFocusable(true);
        initWorld();
    }

    public int getBoardWidth() {
        return this.w;
    }

    public int getBoardHeight() {
        return this.h;
    }

    public final void initWorld() {
        
        int x = OFFSET;
        int y = OFFSET;
        
        Wall wall;
        Baggage b;
        Area a;


        for (int i = 0; i &lt; level.length(); i++) {

            char item = level.charAt(i);

            if (item == '\n') {
                y += SPACE;
                if (this.w &lt; x) {
                    this.w = x;
                }

                x = OFFSET;
            } else if (item == '#') {
                wall = new Wall(x, y);
                walls.add(wall);
                x += SPACE;
            } else if (item == '$') {
                b = new Baggage(x, y);
                baggs.add(b);
                x += SPACE;
            } else if (item == '.') {
                a = new Area(x, y);
                areas.add(a);
                x += SPACE;
            } else if (item == '@') {
                soko = new Player(x, y);
                x += SPACE;
            } else if (item == ' ') {
                x += SPACE;
            }

            h = y;
        }
    }

    public void buildWorld(Graphics g) {

        g.setColor(new Color(250, 240, 170));
        g.fillRect(0, 0, this.getWidth(), this.getHeight());

        ArrayList world = new ArrayList();
        world.addAll(walls);
        world.addAll(areas);
        world.addAll(baggs);
        world.add(soko);

        for (int i = 0; i &lt; world.size(); i++) {

            Actor item = (Actor) world.get(i);

            if ((item instanceof Player)
                    || (item instanceof Baggage)) {
                g.drawImage(item.getImage(), item.x() + 2, item.y() + 2, this);
            } else {
                g.drawImage(item.getImage(), item.x(), item.y(), this);
            }

            if (completed) {
                g.setColor(new Color(0, 0, 0));
                g.drawString("Completed", 25, 20);
            }

        }
    }

    @Override
    public void paint(Graphics g) {
        super.paint(g);
        buildWorld(g);
    }

    class TAdapter extends KeyAdapter {

        @Override
        public void keyPressed(KeyEvent e) {

            if (completed) {
                return;
            }

            
            int key = e.getKeyCode();


            if (key == KeyEvent.VK_LEFT) {
                if (checkWallCollision(soko,
                        LEFT_COLLISION)) {
                    return;
                }

                if (checkBagCollision(LEFT_COLLISION)) {
                    return;
                }

                soko.move(-SPACE, 0);

            } else if (key == KeyEvent.VK_RIGHT) {

                if (checkWallCollision(soko,
                        RIGHT_COLLISION)) {
                    return;
                }

                if (checkBagCollision(RIGHT_COLLISION)) {
                    return;
                }

                soko.move(SPACE, 0);

            } else if (key == KeyEvent.VK_UP) {

                if (checkWallCollision(soko,
                        TOP_COLLISION)) {
                    return;
                }

                if (checkBagCollision(TOP_COLLISION)) {
                    return;
                }

                soko.move(0, -SPACE);

            } else if (key == KeyEvent.VK_DOWN) {

                if (checkWallCollision(soko,
                        BOTTOM_COLLISION)) {
                    return;
                }

                if (checkBagCollision(BOTTOM_COLLISION)) {
                    return;
                }

                soko.move(0, SPACE);

            } else if (key == KeyEvent.VK_R) {
                restartLevel();
            }

            repaint();
        }
    }

    private boolean checkWallCollision(Actor actor, int type) {

        if (type == LEFT_COLLISION) {

            for (int i = 0; i &lt; walls.size(); i++) {
                Wall wall = (Wall) walls.get(i);
                if (actor.isLeftCollision(wall)) {
                    return true;
                }
            }
            return false;

        } else if (type == RIGHT_COLLISION) {

            for (int i = 0; i &lt; walls.size(); i++) {
                Wall wall = (Wall) walls.get(i);
                if (actor.isRightCollision(wall)) {
                    return true;
                }
            }
            return false;

        } else if (type == TOP_COLLISION) {

            for (int i = 0; i &lt; walls.size(); i++) {
                Wall wall = (Wall) walls.get(i);
                if (actor.isTopCollision(wall)) {
                    return true;
                }
            }
            return false;

        } else if (type == BOTTOM_COLLISION) {

            for (int i = 0; i &lt; walls.size(); i++) {
                Wall wall = (Wall) walls.get(i);
                if (actor.isBottomCollision(wall)) {
                    return true;
                }
            }
            return false;
        }
        return false;
    }

    private boolean checkBagCollision(int type) {

        if (type == LEFT_COLLISION) {

            for (int i = 0; i &lt; baggs.size(); i++) {

                Baggage bag = (Baggage) baggs.get(i);
                if (soko.isLeftCollision(bag)) {

                    for (int j=0; j &lt; baggs.size(); j++) {
                        Baggage item = (Baggage) baggs.get(j);
                        if (!bag.equals(item)) {
                            if (bag.isLeftCollision(item)) {
                                return true;
                            }
                        }
                        if (checkWallCollision(bag,
                                LEFT_COLLISION)) {
                            return true;
                        }
                    }
                    bag.move(-SPACE, 0);
                    isCompleted();
                }
            }
            return false;

        } else if (type == RIGHT_COLLISION) {

            for (int i = 0; i &lt; baggs.size(); i++) {

                Baggage bag = (Baggage) baggs.get(i);
                if (soko.isRightCollision(bag)) {
                    for (int j=0; j &lt; baggs.size(); j++) {

                        Baggage item = (Baggage) baggs.get(j);
                        if (!bag.equals(item)) {
                            if (bag.isRightCollision(item)) {
                                return true;
                            }
                        }
                        if (checkWallCollision(bag,
                                RIGHT_COLLISION)) {
                            return true;
                        }
                    }
                    bag.move(SPACE, 0);
                    isCompleted();                   
                }
            }
            return false;

        } else if (type == TOP_COLLISION) {

            for (int i = 0; i &lt; baggs.size(); i++) {

                Baggage bag = (Baggage) baggs.get(i);
                if (soko.isTopCollision(bag)) {
                    for (int j = 0; j &lt; baggs.size(); j++) {

                        Baggage item = (Baggage) baggs.get(j);
                        if (!bag.equals(item)) {
                            if (bag.isTopCollision(item)) {
                                return true;
                            }
                        }
                        if (checkWallCollision(bag,
                                TOP_COLLISION)) {
                            return true;
                        }
                    }
                    bag.move(0, -SPACE);
                    isCompleted();
                }
            }

            return false;

        } else if (type == BOTTOM_COLLISION) {
        
            for (int i = 0; i &lt; baggs.size(); i++) {

                Baggage bag = (Baggage) baggs.get(i);
                if (soko.isBottomCollision(bag)) {
                    for (int j = 0; j &lt; baggs.size(); j++) {

                        Baggage item = (Baggage) baggs.get(j);
                        if (!bag.equals(item)) {
                            if (bag.isBottomCollision(item)) {
                                return true;
                            }
                        }
                        if (checkWallCollision(bag,
                                BOTTOM_COLLISION)) {
                            return true;
                        }
                    }
                    bag.move(0, SPACE);
                    isCompleted();
                }
            }
        }

        return false;
    }

    public void isCompleted() {

        int num = baggs.size();
        int compl = 0;

        for (int i = 0; i &lt; num; i++) {
            Baggage bag = (Baggage) baggs.get(i);
            for (int j = 0; j &lt; num; j++) {
                Area area = (Area) areas.get(j);
                if (bag.x() == area.x()
                        &amp;&amp; bag.y() == area.y()) {
                    compl += 1;
                }
            }
        }

        if (compl == num) {
            completed = true;
            repaint();
        }
    }

    public void restartLevel() {

        areas.clear();
        baggs.clear();
        walls.clear();
        initWorld();
        if (completed) {
            completed = false;
        }
    }
}
</pre>


<p>
The game is simplified. It only provides the very basic functionality.
The code is than easier to understand. The game has one level. 
</p>

<pre class="explanation">
private final int OFFSET = 30;
private final int SPACE = 20;
private final int LEFT_COLLISION = 1;
private final int RIGHT_COLLISION = 2;
private final int TOP_COLLISION = 3;
private final int BOTTOM_COLLISION = 4;
</pre>

<p>
The wall image size is 20x20px. This reflects the SPACE constant. The OFFSET is 
the distance between the borders of the window and the game world. There are four 
types of collisions. Each one is represented by a numerical constant. 
</p>

<pre class="explanation">
private ArrayList walls = new ArrayList();
private ArrayList baggs = new ArrayList();
private ArrayList areas = new ArrayList();
</pre>

<p>
The walls, baggs and areas are special containers, which will hold
all the walls, baggs and areas of the game. 
</p>

<pre class="explanation">
private String level =
          "    ######\n"
        + "    ##   #\n"
        + "    ##$  #\n"
        + "  ####  $##\n"
        + "  ##  $ $ #\n"
        + "#### # ## #   ######\n"
        + "##   # ## #####  ..#\n"
        + "## $  $          ..#\n"
        + "###### ### #@##  ..#\n"
        + "    ##     #########\n"
        + "    ########\n";
</pre>

<p>
This is the level of the game. Except for the space, there are
five characters. The hash (#) stands for a wall. The dollar ($) represents the box
to move. The dot (.) character represents the place where we must move the box. 
The at character (@) is the sokoban. And finally the new line character (\n)
starts a new row of the world.
</p>


<pre class="explanation">
public final void initWorld() {
    
    int x = OFFSET;
    int y = OFFSET;
...
</pre>

<p>
The initWorld() method initiates the game world. It goes through 
the level string and fills the above mentioned lists. 
</p>



<pre class="explanation">
} else if (item == '$') {
    b = new Baggage(x, y);
    baggs.add(b);
    x += SPACE;
</pre>

<p>
In case of the dollar character, we create a 
Baggage object. The object is appended
to the baggs list. The x variable is increased accordingly.
</p>

<pre class="explanation">
public void buildWorld(Graphics g) {
...
</pre>

<p>
The buildWorld() method draws the game world on the window. 
</p>

<pre class="explanation">
ArrayList world = new ArrayList();
world.addAll(walls);
world.addAll(areas);
world.addAll(baggs);
world.add(soko);
</pre>

<p>
We create a world list which includes all objects of the game. 
</p>

<pre class="explanation">
for (int i = 0; i < world.size(); i++) {

    Actor item = (Actor) world.get(i);

    if ((item instanceof Player)
            || (item instanceof Baggage)) {
        g.drawImage(item.getImage(), item.x() + 2, item.y() + 2, this);
    } else {
        g.drawImage(item.getImage(), item.x(), item.y(), this);
    }
...
</pre>

<p>
We iterate through the world container and draw the objects.
The player and the baggage images are a bit smaller. We add
2px to their coordinates to center them. 
</p>

<pre class="explanation">
if (completed) {
    g.setColor(new Color(0, 0, 0));
    g.drawString("Completed", 25, 20);
}
</pre>

<p>
If the level is completed, we draw "Completed" in the upper left corner 
of the window. 
</p>


<pre class="explanation">
if (key == KeyEvent.VK_LEFT) {
    if (checkWallCollision(soko,
            LEFT_COLLISION)) {
        return;
    }

    if (checkBagCollision(LEFT_COLLISION)) {
        return;
    }

    soko.move(-SPACE, 0);
...
</pre>

<p>
Inside the keyPressed() method, we check what keys were pressed. We control 
the sokoban object with the cursor keys. If we press the left cursor key,
we check if the sokoban collides with a wall or with a 
baggage. If it does not, we move the sokoban to the left.
</p>

<pre class="explanation">
} else if (key == KeyEvent.VK_R) {
    restartLevel();
}
</pre>

<p>
We restart the level, if we press the R key.
</p>

<pre class="explanation">
if (type == LEFT_COLLISION) {

    for (int i = 0; i &lt; walls.size(); i++) {
        Wall wall = (Wall) walls.get(i);
        if (actor.isLeftCollision(wall)) {
            return true;
        }
    }
    return false;
...
</pre>

<p>
The checkWallCollision() method was created to
ensure, that the sokoban or a baggage don't pass the 
wall. There are four types of collisions. The above lines
check for the left collision. 
</p>

<pre class="explanation">
private boolean checkBagCollision(int type) {

}
</pre>

<p>
The checkBagCollision() is a bit more involved. A baggage
can collide with a wall, with a sokoban object or with another
baggage. The baggage can be moved only if it collides with a sokoban and
does not collide with another baggage or a wall. When the baggage is moved,
it is time to check, if the level is completed by calling the
isCompleted() method. 
</p>

<pre class="explanation">
for (int i = 0; i &lt; num; i++) {
    Baggage bag = (Baggage) baggs.get(i);
    for (int j = 0; j &lt; num; j++) {
        Area area = (Area) areas.get(j);
        if (bag.x() == area.x()
                &amp;&amp; bag.y() == area.y()) {
            compl += 1;
        }
    }
}
</pre>

<p>
The isCompleted() method checks, if the level is completed. We get the number 
of bags. We compare the x, y coordinates of all the bags and the destination areas. 
</p>

<pre class="explanation">
if (compl == num) {
    completed = true;
    repaint();
}
</pre>

<p>
The game is finished, when the completed
variable equals the number of bags in the game. 
</p>

<pre class="explanation">
public void restartLevel() {

    areas.clear();
    baggs.clear();
    walls.clear();
    initWorld();
    if (completed) {
        completed = false;
    }
}
</pre>

<p>
If we do some bad move, we can restart the level. We delete 
all objects from the important lists and initiate the world
again. The completed variable is set to false. 
</p>

<div class="codehead">Actor.java</div>
<pre class="code">
package sokoban;

import java.awt.Image;

public class Actor {

    private final int SPACE = 20;

    private int x;
    private int y;
    private Image image;

    public Actor(int x, int y) {
        this.x = x;
        this.y = y;
    }

    public Image getImage() {
        return this.image;
    }

    public void setImage(Image img) {
        image = img;
    }

    public int x() {
        return this.x;
    }

    public int y() {
        return this.y;
    }

    public void setX(int x) {
        this.x = x;
    }

    public void setY(int y) {
        this.y = y;
    }

    public boolean isLeftCollision(Actor actor) {
        if (((this.x() - SPACE) == actor.x()) &amp;&amp;
            (this.y() == actor.y())) {
            return true;
        } else {
            return false;
        }
    }

    public boolean isRightCollision(Actor actor) {
        if (((this.x() + SPACE) == actor.x())
                &amp;&amp; (this.y() == actor.y())) {
            return true;
        } else {
            return false;
        }
    }

    public boolean isTopCollision(Actor actor) {
        if (((this.y() - SPACE) == actor.y()) &amp;&amp;
            (this.x() == actor.x())) {
            return true;
        } else {
            return false;
        }
    }

    public boolean isBottomCollision(Actor actor) {
        if (((this.y() + SPACE) == actor.y())
                &amp;&amp; (this.x() == actor.x())) {
            return true;
        } else {
            return false;
        }
    }
}
</pre>

<p>
This is the Actor class. The class is a base class for other 
actors in the game. It encapsulates the basic functionality 
of an object in the Sokoban game. 
</p>

<pre class="explanation">
public boolean isLeftCollision(Actor actor) {
    if (((this.x() - SPACE) == actor.x()) &amp;&amp;
        (this.y() == actor.y())) {
        return true;
    } else {
        return false;
    }
}
</pre>

<p>
This method checks, if the actor collides with another
actor (wall, baggage, sokoban) to the left. 
</p>

<div class="codehead">Wall.java</div>
<pre class="code">
package sokoban;

import java.awt.Image;
import java.net.URL;
import javax.swing.ImageIcon;

public class Wall extends Actor {

    private Image image;

    public Wall(int x, int y) {
        super(x, y);

        URL loc = this.getClass().getResource("wall.png");
        ImageIcon iia = new ImageIcon(loc);
        image = iia.getImage();
        this.setImage(image);

    }
}
</pre>

<p>
This is the Wall class. It inherits from the Actor class. Upon construction, it 
loads a wall image from the filesystem.
</p>


<div class="codehead">Player.java</div>
<pre class="code">
package sokoban;

import java.awt.Image;
import java.net.URL;
import javax.swing.ImageIcon;

public class Player extends Actor {

    public Player(int x, int y) {
        super(x, y);

        URL loc = this.getClass().getResource("sokoban.png");
        ImageIcon iia = new ImageIcon(loc);
        Image image = iia.getImage();
        this.setImage(image);
    }

    public void move(int x, int y) {
        int nx = this.x() + x;
        int ny = this.y() + y;
        this.setX(nx);
        this.setY(ny);
    }
}
</pre>

<p>
This is the Player class. It is the class to create the sokoban object. 
</p>

<pre class="explanation">
public void move(int x, int y) {
    int nx = this.x() + x;
    int ny = this.y() + y;
    this.setX(nx);
    this.setY(ny);
}
</pre>

<p>
This class has a move() method, which moves the object inside the world. 
</p>

<div class="codehead">Baggage.java</div>
<pre class="code">
package sokoban;

import java.awt.Image;
import java.net.URL;
import javax.swing.ImageIcon;

public class Baggage extends Actor {

    public Baggage(int x, int y) {
        super(x, y);
        URL loc = this.getClass().getResource("baggage.png");
        ImageIcon iia = new ImageIcon(loc);
        Image image = iia.getImage();
        this.setImage(image);
    }

    public void move(int x, int y) {
        int nx = this.x() + x;
        int ny = this.y() + y;
        this.setX(nx);
        this.setY(ny);
    }
}
</pre>

<p>
This is the class for the Baggage object. This object is movable, so it has the move()
method also. 
</p>

<div class="codehead">Area.java</div>
<pre class="code">
package sokoban;

import java.awt.Image;
import java.net.URL;
import javax.swing.ImageIcon;

public class Area extends Actor {

    public Area(int x, int y) {
        super(x, y);

        URL loc = this.getClass().getResource("area.png");
        ImageIcon iia = new ImageIcon(loc);
        Image image = iia.getImage();
        this.setImage(image);
    }
}
</pre>

<p>
The Area class. It is the object, on which we
try to place the baggages. 
</p>

<div class="codehead">Sokoban.java</div>
<pre class="code">
package sokoban;

import javax.swing.JFrame;


public final class Sokoban extends JFrame {

    private final int OFFSET = 30;

    public Sokoban() {
        InitUI();
    }

    public void InitUI() {
        Board board = new Board();
        add(board);

        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        setSize(board.getBoardWidth() + OFFSET,
                board.getBoardHeight() + 2*OFFSET);
        setLocationRelativeTo(null);
        setTitle("Sokoban");
    }


    public static void main(String[] args) {
        Sokoban sokoban = new Sokoban();
        sokoban.setVisible(true);
    }
}
</pre>

<p>
This is the main class.  
</p>


<img src="/img/gfx/javagames/sokoban.png" alt="Sokoban">
<div class="figure">Figure: Sokoban</div>


<p>
This was the Sokoban game. 
</p>

<div class="center"> 
<script type="text/javascript"><!--
google_ad_client = "pub-9706709751191532";
/* horizontal */
google_ad_slot = "1734478269";
google_ad_width = 468;
google_ad_height = 60;
//-->
</script> 
<script type="text/javascript"
src="http://pagead2.googlesyndication.com/pagead/show_ads.js"> 
</script> 
</div>
<br>


<div class="botNav, center">
<span class="botNavItem"><a href="/">Home</a></span> ‡ <span class="botNavItem"><a href="..">Contents</a></span> ‡ 
<span class="botNavItem"><a href="#">Top of Page</a></span>
</div>


<div class="footer">
<div class="signature">
<a href="/">ZetCode</a> last modified October 7, 2010  <span class="copyright">&copy; 2007 - 2013 Jan Bodnar</span>
</div>
</div>

</div> <!-- content -->

</div> <!-- container -->

</body>
</html>

